package com.ctrip.framework.apollo.portal.service;

import com.ctrip.framework.apollo.common.entity.App;
import com.ctrip.framework.apollo.common.entity.AppNamespace;
import com.ctrip.framework.apollo.common.exception.BadRequestException;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.portal.repository.AppNamespaceRepository;
import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Objects;
import java.util.Set;

@Service
public class AppNamespaceService {

    private static final int PRIVATE_APP_NAMESPACE_NOTIFICATION_COUNT = 5;
    private static final Joiner APP_NAMESPACE_JOINER = Joiner.on(",").skipNulls();

    private final UserInfoHolder userInfoHolder;
    private final AppNamespaceRepository appNamespaceRepository;
    private final RoleInitializationService roleInitializationService;
    private final AppService appService;
    private final RolePermissionService rolePermissionService;

    public AppNamespaceService(
            final UserInfoHolder userInfoHolder,
            final AppNamespaceRepository appNamespaceRepository,
            final RoleInitializationService roleInitializationService,
            final @Lazy AppService appService,
            final RolePermissionService rolePermissionService) {
        this.userInfoHolder = userInfoHolder;
        this.appNamespaceRepository = appNamespaceRepository;
        this.roleInitializationService = roleInitializationService;
        this.appService = appService;
        this.rolePermissionService = rolePermissionService;
    }

    /**
     * 公共的app ns,能被其它项目关联到的app ns
     */
    public List<AppNamespace> findPublicAppNamespaces() {
        return appNamespaceRepository.findByIsPublicTrue();
    }

    public AppNamespace findPublicAppNamespace(String namespaceName) {
        List<AppNamespace> appNamespaces = appNamespaceRepository.findByNameAndIsPublic(namespaceName, true);
        if (CollectionUtils.isEmpty(appNamespaces)) {
            return null;
        }
        return appNamespaces.get(0);
    }

    private List<AppNamespace> findAllPrivateAppNamespaces(String namespaceName) {
        return appNamespaceRepository.findByNameAndIsPublic(namespaceName, false);
    }

    public AppNamespace findByAppIdAndName(String appId, String namespaceName) {
        return appNamespaceRepository.findByAppIdAndName(appId, namespaceName);
    }

    public List<AppNamespace> findByAppId(String appId) {
        return appNamespaceRepository.findByAppId(appId);
    }

    public List<AppNamespace> findAll() {
        Iterable<AppNamespace> appNamespaces = appNamespaceRepository.findAll();
        return Lists.newArrayList(appNamespaces);
    }

    @Transactional
    public void createDefaultAppNamespace(String appId) {
        if (!isAppNamespaceNameUnique(appId, ConfigConsts.NAMESPACE_APPLICATION)) {
            throw new BadRequestException(String.format("App already has application namespace. AppId = %s", appId));
        }
        AppNamespace appNs = new AppNamespace();
        appNs.setAppId(appId);
        appNs.setName(ConfigConsts.NAMESPACE_APPLICATION);
        appNs.setComment("default app namespace");
        appNs.setFormat(ConfigFileFormat.Properties.getValue());
        String userId = userInfoHolder.getUser().getUserId();
        appNs.setDataChangeCreatedBy(userId);
        appNs.setDataChangeLastModifiedBy(userId);
        appNamespaceRepository.save(appNs);
    }

    public boolean isAppNamespaceNameUnique(String appId, String namespaceName) {
        Objects.requireNonNull(appId, "AppId must not be null");
        Objects.requireNonNull(namespaceName, "Namespace must not be null");
        return Objects.isNull(appNamespaceRepository.findByAppIdAndName(appId, namespaceName));
    }

    public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace) {
        return createAppNamespaceInLocal(appNamespace, true);
    }

    @Transactional
    public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace, boolean appendNamespacePrefix) {
        String appId = appNamespace.getAppId();

        // add app org id as prefix
        App app = appService.load(appId);
        if (app == null) {
            throw new BadRequestException("App not exist. AppId = " + appId);
        }

        // add prefix postfix
        String appNamespaceName = (appNamespace.isPublic() && appendNamespacePrefix ? app.getOrgId() + "." : "") + appNamespace.getName() + (appNamespace.formatAsEnum() == ConfigFileFormat.Properties ? "" : "." + appNamespace.getFormat());
        appNamespace.setName(appNamespaceName);
        if (appNamespace.getComment() == null) {
            appNamespace.setComment("");
        }

        if (!ConfigFileFormat.isValidFormat(appNamespace.getFormat())) {
            throw new BadRequestException("Invalid namespace format. format must be properties、json、yaml、yml、xml");
        }

        String operator = appNamespace.getDataChangeCreatedBy();
        if (StringUtils.isEmpty(operator)) {
            operator = userInfoHolder.getUser().getUserId();
            appNamespace.setDataChangeCreatedBy(operator);
        }

        appNamespace.setDataChangeLastModifiedBy(operator);

        // globally uniqueness check for public app namespace
        if (appNamespace.isPublic()) {
            checkAppNamespaceGlobalUniqueness(appNamespace);
        } else {
            // check private app namespace
            if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) {
                throw new BadRequestException("Private AppNamespace " + appNamespace.getName() + " already exists!");
            }
            // should not have the same with public app namespace
            checkPublicAppNamespaceGlobalUniqueness(appNamespace);
        }

        AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace);

        roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(), operator);
        roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(), operator);

        return createdAppNamespace;
    }

    @Transactional
    public AppNamespace importAppNamespaceInLocal(AppNamespace appNamespace) {
        // globally uniqueness check for public app namespace
        if (appNamespace.isPublic()) {
            checkAppNamespaceGlobalUniqueness(appNamespace);
        } else {
            // check private app namespace
            if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) {
                throw new BadRequestException("Private AppNamespace " + appNamespace.getName() + " already exists!");
            }
            // should not have the same with public app namespace
            checkPublicAppNamespaceGlobalUniqueness(appNamespace);
        }

        AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace);

        String operator = appNamespace.getDataChangeCreatedBy();

        roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(), operator);
        roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(), operator);

        return createdAppNamespace;
    }

    private void checkAppNamespaceGlobalUniqueness(AppNamespace appNamespace) {
        checkPublicAppNamespaceGlobalUniqueness(appNamespace);

        List<AppNamespace> privateAppNamespaces = findAllPrivateAppNamespaces(appNamespace.getName());
        if (!CollectionUtils.isEmpty(privateAppNamespaces)) {
            Set<String> appIds = Sets.newHashSet();
            for (AppNamespace ans : privateAppNamespaces) {
                appIds.add(ans.getAppId());
                if (appIds.size() == PRIVATE_APP_NAMESPACE_NOTIFICATION_COUNT) {
                    break;
                }
            }
            throw new BadRequestException("Public AppNamespace " + appNamespace.getName() + " already exists as private AppNamespace in appId: " + APP_NAMESPACE_JOINER.join(appIds) + ", etc. Please select another name!");
        }
    }

    private void checkPublicAppNamespaceGlobalUniqueness(AppNamespace appNamespace) {
        AppNamespace publicAppNamespace = findPublicAppNamespace(appNamespace.getName());
        if (publicAppNamespace != null) {
            throw new BadRequestException("AppNamespace " + appNamespace.getName() + " already exists as public namespace in appId: " + publicAppNamespace.getAppId() + "!");
        }
    }

    @Transactional
    public AppNamespace deleteAppNamespace(String appId, String namespaceName) {
        AppNamespace appNamespace = appNamespaceRepository.findByAppIdAndName(appId, namespaceName);
        if (appNamespace == null) {
            throw new BadRequestException(String.format("AppNamespace not exists. AppId = %s, NamespaceName = %s", appId, namespaceName));
        }

        String operator = userInfoHolder.getUser().getUserId();

        // this operator is passed to com.ctrip.framework.apollo.portal.listener.DeletionListener.onAppNamespaceDeletionEvent
        appNamespace.setDataChangeLastModifiedBy(operator);

        // delete app namespace in portal db
        appNamespaceRepository.delete(appId, namespaceName, operator);

        // delete Permission and Role related data
        rolePermissionService.deleteRolePermissionsByAppIdAndNamespace(appId, namespaceName, operator);

        return appNamespace;
    }

    public void batchDeleteByAppId(String appId, String operator) {
        appNamespaceRepository.batchDeleteByAppId(appId, operator);
    }
}
